package com.googlecode.cswish.util;

import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;

import javax.annotation.Resource;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.googlecode.cswish.annotation.ParamScope;
import com.googlecode.cswish.model.ModelInfo;
import com.googlecode.cswish.model.PropertyInfo;
import com.googlecode.cswish.struts.Invoker;
import com.googlecode.cswish.struts.spring.BeanIntrospector;
import com.opensymphony.xwork2.ActionContext;
import com.opensymphony.xwork2.conversion.impl.XWorkConverter;
import com.opensymphony.xwork2.util.ValueStack;
import com.opensymphony.xwork2.util.reflection.ReflectionContextState;

/**
 * A util class to call service method
 * 
 * @author feng Date: 2009-7-9
 */
public class InvocationUtil {

	private static final Log logger = LogFactory.getLog(InvocationUtil.class);

	@Resource
	BeanIntrospector beanIntrospector;

	@Resource
	OgnlExUtil ognlExUtil;

	Map<String, Object> annotationCache = new HashMap<String, Object>();
	Object NULL = new Object();

	private final Map<Class, Object> primitiveDefaults;

	public InvocationUtil() {
		Map<Class, Object> map = new HashMap<Class, Object>();
		map.put(Boolean.TYPE, Boolean.FALSE);
		map.put(Byte.TYPE, new Byte((byte) 0));
		map.put(Short.TYPE, new Short((short) 0));
		map.put(Character.TYPE, new Character((char) 0));
		map.put(Integer.TYPE, new Integer(0));
		map.put(Long.TYPE, new Long(0L));
		map.put(Float.TYPE, new Float(0.0f));
		map.put(Double.TYPE, new Double(0.0));
		map.put(BigInteger.class, new BigInteger("0"));
		map.put(BigDecimal.class, new BigDecimal(0.0));
		primitiveDefaults = Collections.unmodifiableMap(map);
	}

	/**
	 * Find the annotation definition (support to load from cache)
	 * 
	 * @param defaultKey
	 * @param method
	 * @return
	 */
	public ParamScope getActionParamScope(String defaultKey, Method method) {
		Object paramScope = annotationCache.get(defaultKey);
		if (paramScope == null && method != null) {
			paramScope = method.getAnnotation(ParamScope.class);
			if (paramScope == null) {
				Class<?>[] types = method.getParameterTypes();
				if (types.length == 1) {
					paramScope = types[0].getAnnotation(ParamScope.class);
				}
			}

			if (paramScope != null) {
				annotationCache.put(defaultKey, paramScope);
			} else {
				annotationCache.put(defaultKey, NULL);
			}
		}
		return (ParamScope) (paramScope == NULL ? null : paramScope);
	}

	/**
	 * Call ParamScope to find the parameter value from the life-cycle manager
	 * 
	 * @param serviceClass
	 * @param methodName
	 * @param parameterName
	 * @return
	 */
	public Object getServiceParameterValue(Class serviceClass, String methodName, String parameterName) {
		String simpleName = beanIntrospector.getSimpleName(serviceClass.getName());
		String key = getDefaultKey(simpleName, methodName, parameterName);

		Object paramScope = annotationCache.get(key);
		if (paramScope == null) {

			String actionName = beanIntrospector.getActionName(simpleName + '.' + methodName);
			ModelInfo modelInfo = beanIntrospector.getModelInfo(actionName);
			String writeMethod = "set" + StringUtils.capitalize(parameterName);
			String protectedWriteMethod = "set" + FrameConstant.PARAM_PROCTED_NAME + parameterName;
			Method theMethod = null;
			for (PropertyInfo propertyInfo : modelInfo.getInterfaceProperties()) {
				Method method = propertyInfo.getWriteMethod();
				if (method != null && (method.getName().equals(writeMethod)
						|| method.getName().equals(protectedWriteMethod))) {
					theMethod = method;
					break;
				}
			}

			if (theMethod != null) {
				paramScope = getActionParamScope(key, theMethod);
			} else {
				annotationCache.put(key, NULL);
			}
		}

		if (paramScope != null && paramScope != NULL) {
			ActionContext context = ActionContext.getContext();
			String scopeKey = getScopeKey(context, key, serviceClass.getName(), methodName, parameterName,
					(ParamScope) paramScope);
			return findValueFromParamScope(context, scopeKey, serviceClass.getName(), methodName, parameterName,
					(ParamScope) paramScope);
		}
		return null;
	}

	public String getDefaultKey(String simpleName, String methodName, String parameterName) {
		return simpleName + '@' + methodName + '@' + parameterName;
	}

	public String getScopeKey(ActionContext context, String defaultKey, String className, String methodName,
			String parameterName, ParamScope paramScope) {
		ValueStack stack = context.getValueStack();
		String key;
		if (StringUtils.isEmpty(paramScope.scopeKey())) {
			key = defaultKey;
		} else {
			try {
				ReflectionContextState.setDenyMethodExecution(context.getContextMap(), false);
				Map<String, String> propInfo = new HashMap<String, String>(5);
				Invoker invoker = new Invoker(className);
				propInfo.put("className", beanIntrospector.getSimpleName(className));
				propInfo.put("methodName", invoker.methodName);
				propInfo.put("propertyName", parameterName);
				stack.getContext().put("propInfo", propInfo);
				key = stack.findString(paramScope.scopeKey());
			} finally {
				ReflectionContextState.setDenyMethodExecution(context.getContextMap(), true);
			}
		}
		return key;
	}

	/**
	 * Call ParamScope to find the parameter value from the life-cycle manager
	 * 
	 * @param serviceClass
	 * @param methodName
	 * @param parameterName
	 * @return
	 */
	public Object findValueFromParamScope(ActionContext context, String scopeKey, String className, String methodName,
			String parameterName, ParamScope paramScope) {
		Object oldValue = null;
		// first, find the old value
		switch (paramScope.scope()) {
		case SESSION:
			oldValue = context.getSession().get(scopeKey);
			break;
		case REQUEST:
			oldValue = context.getValueStack().findValue(scopeKey);
			break;
		case APPLICATION:
			oldValue = context.getApplication().get(scopeKey);
			break;
		case ALL:
			break;
		}

		// second, try to initialize it according to the OGNL expression
		if (oldValue == null && paramScope.value().length() > 0) {
			try {
				ReflectionContextState.setDenyMethodExecution(context.getContextMap(), false);
				oldValue = context.getValueStack().findValue(paramScope.value());
			} finally {
				ReflectionContextState.setDenyMethodExecution(context.getContextMap(), true);
			}
		}
		return oldValue;
	}

	/**
	 * Invoke a service object and prepare the Scope Parameter value
	 * 
	 * @param service
	 * @param methodName
	 * @param parameters
	 * @return
	 */
	public <T> T invoke(Object service, String methodName, Map<String, Object> parameters) {
		Map<String, Object> defaultParameters = new LinkedHashMap<String, Object>();

		Method theMethod = null;
		for (Method method : service.getClass().getMethods()) {
			if (method.getName().equals(methodName)) {
				theMethod = method;
				String[] parameterNames = ClassEx.getParameterNames(method);
				Class<?>[] paramTypes = method.getParameterTypes();
				Class<?> paramType;
				int index = 0;
				XWorkConverter converter = ognlExUtil.getXWorkConverter();
				for (String theName : parameterNames) {
					paramType = paramTypes[index++];
					Object value = null;
					if (parameters != null && parameters.containsKey(theName)) {
						value = parameters.get(theName);
					} else {
						// the parameter isn't passed, load it from ParamScope
						value = getServiceParameterValue(service.getClass(), methodName, theName);
					}

					if (value == null && paramType.isPrimitive()) {
						value = primitiveDefaults.get(paramType);
					} else if (value != null && !paramType.isAssignableFrom(value.getClass())) {
						value = converter.convertValue(value, paramType);
					}
					// set parameter value and prepare the parameter order
					defaultParameters.put(theName, value); 
				}
				break;
			}
		}

		if (theMethod != null) {
			try {
				Object[] theParameters = defaultParameters.values().toArray();
				return (T) theMethod.invoke(service, theParameters);
			} catch (Exception ex) {
				logger.error("Fail to invoke service:" + service.getClass().getName() + '.' + methodName, ex);
			}
		}
		return null;
	}
}